/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.spark.ui

import javax.servlet.http.HttpServletRequest

import scala.collection.mutable.ArrayBuffer
import scala.collection.mutable.HashMap
import scala.xml.Node

import org.eclipse.jetty.servlet.ServletContextHandler
import org.json4s.JsonAST.{JNothing, JValue}

import org.apache.spark.{SecurityManager, SparkConf, SSLOptions}
import org.apache.spark.internal.Logging
import org.apache.spark.internal.config._
import org.apache.spark.ui.JettyUtils._
import org.apache.spark.util.Utils

/**
 * WEBUI定义了一种界面展示的框架，并提供返回Json格式数据的Web服务。
 * 成员属性：
 * securityManager：SparkEnv中创建的安全管理器securityManager
 * sslOptions：使用后SecurityManager获取spark.ssl.ui属性指定的WebUI的SSL（Secure Sockets Layer,安全套阶层选项）
 * port：WebUi对外服务的端口，可以使用spark.ui.port进行配置
 * conf：就是sparkCONF
 * basePath:WebUI的基本路径
 * name：WebUI的名称，Spark UI的name为SparkUI
 * The top level component of the UI hierarchy that contains the server.
 *
 * Each WebUI represents a collection of tabs, each of which in turn represents a collection of
 * pages. The use of tabs is optional, however; a WebUI may choose to include pages directly.
 */
private[spark] abstract class WebUI(
    val securityManager: SecurityManager,
    val sslOptions: SSLOptions,
    port: Int,
    conf: SparkConf,
    basePath: String = "",
    name: String = "")
  extends Logging {

  //WebUITab的缓冲数组
  protected val tabs = ArrayBuffer[WebUITab]()
  //ServletContextHandler的缓冲数组。ServletContextHandler是Jetty提供的API,负责对ServletContext进行处理。
  protected val handlers = ArrayBuffer[ServletContextHandler]()
  //WebUIPage与ServeletContextHandler缓冲数组之间的映射关系。
  // 由于WebUIPage的两个方法reender和renderJson分别需要有一个对应的ServletContextHandler处理，所以一个WebUiPage对应两个ServletContextHandler
  protected val pageToHandlers = new HashMap[WebUIPage, ArrayBuffer[ServletContextHandler]]
  //serverInfo：用于缓存serverInfo，即WebUI的Jetty服务器信息
  protected var serverInfo: Option[ServerInfo] = None
  //publicHostName：当前WebUI的Jetty服务的主机名.优先采用SPARK_PUBLIC_DNS指定的主机名，否则采用spark.driver.host属性指定的host
  //在这两个都没有配置的时候，将默认使用工具类Utils的localHostName方法
  protected val publicHostName = Option(conf.getenv("SPARK_PUBLIC_DNS")).getOrElse(
    conf.get(DRIVER_HOST_ADDRESS))
  //过滤了$符号的当前类的简单名称
  private val className = Utils.getFormattedClassName(this)

  def getBasePath: String = basePath
  def getTabs: Seq[WebUITab] = tabs.toSeq
  //获取handler中的所有ServletContextHandler，并以scala的序列返回
  def getHandlers: Seq[ServletContextHandler] = handlers.toSeq
  //获取securityManager
  def getSecurityManager: SecurityManager = securityManager

  /** Attach a tab to this UI, along with all of its attached pages. */

  //attachTab：首先向tabs中添加WebUItAB，然后给WebUITab中每个WebUIPage施加attachPage方法
  def attachTab(tab: WebUITab) {
    tab.pages.foreach(attachPage)
    tabs += tab
  }

  def detachTab(tab: WebUITab) {
    tab.pages.foreach(detachPage)
    tabs -= tab
  }

  //与attachPage相反
  def detachPage(page: WebUIPage) {
    pageToHandlers.remove(page).foreach(_.foreach(detachHandler))
  }

  /** Attach a page to this UI. */
  def attachPage(page: WebUIPage) {
    val pagePath = "/" + page.prefix
    //1、首先调用JettyUtils的createServletHandler方法给WebUIPage创建与render和renderJson分别关联的ServletContextHandler
    val renderHandler = createServletHandler(pagePath,
      (request: HttpServletRequest) => page.render(request), securityManager, conf, basePath)
    val renderJsonHandler = createServletHandler(pagePath.stripSuffix("/") + "/json",
      (request: HttpServletRequest) => page.renderJson(request), securityManager, conf, basePath)
    //2、通过attachHandler将这些Handler添加到Handler缓存数组与Jetty服务器中
    attachHandler(renderHandler)
    attachHandler(renderJsonHandler)

    //3、最后把WebUIPage与这两个ServletContextHandler的映射关系更新到pageToHandlers中
    val handlers = pageToHandlers.getOrElseUpdate(page, ArrayBuffer[ServletContextHandler]())
    handlers += renderHandler
  }

  /** Attach a handler to this UI. */
    //给Handler缓存数组中添加ServletContextHandler，并且将此ServletContextHandler通过ServerInfo的addHandler方法添加到Jetty服务器中。
  def attachHandler(handler: ServletContextHandler) {
    handlers += handler
    serverInfo.foreach(_.addHandler(handler))
  }

  /** Detach a handler from this UI. */
    //从handler缓存数组中移除ServletContextHandler,并且将此ServletContextHandler通过ServerInfo方法从Jetty服务器中移除
  def detachHandler(handler: ServletContextHandler) {
    handlers -= handler
    serverInfo.foreach(_.removeHandler(handler))
  }

  /**
   * 处理静态文件服务的ServletContextHandler
   * Add a handler for static content.
   *
   * @param resourceBase Root of where to find resources to serve.
   * @param path Path in UI where to mount the resources.
   */
  def addStaticHandler(resourceBase: String, path: String): Unit = {
    attachHandler(JettyUtils.createStaticHandler(resourceBase, path))
  }

  /**
   * Remove a static content handler.
   *
   * @param path Path in UI to unmount.
   */
  def removeStaticHandler(path: String): Unit = {
    handlers.find(_.getContextPath() == path).foreach(detachHandler)
  }

  /** Initialize all components of the server. */
  //用于初始化WebUI服务中的所有组件，这个需要子类实现
  def initialize(): Unit

  /** Bind to the HTTP server behind this web interface. */
    //启动与WebUI绑定的Jetty服务
  def bind() {
    assert(!serverInfo.isDefined, s"Attempted to bind $className more than once!")
    try {
      val host = Option(conf.getenv("SPARK_LOCAL_IP")).getOrElse("0.0.0.0")
      serverInfo = Some(startJettyServer(host, port, sslOptions, handlers, conf, name))
      logInfo(s"Bound $className to $host, and started at $webUrl")
    } catch {
      case e: Exception =>
        logError(s"Failed to bind $className", e)
        System.exit(1)
    }
  }

  /** Return the url of web interface. Only valid after bind(). */
    //获取WebUI的web界面的URL
  def webUrl: String = s"http://$publicHostName:$boundPort"

  /** Return the actual port to which this server is bound. Only valid after bind(). */
    //获取WebUI的Jetty服务的端口
  def boundPort: Int = serverInfo.map(_.boundPort).getOrElse(-1)

  /** Stop the server behind this web interface. Only valid after bind(). */
    //停止
  def stop() {
    assert(serverInfo.isDefined,
      s"Attempted to stop $className before binding to a server!")
    serverInfo.get.stop()
  }
}


/**
 * parent:上一级节点，即父亲。WebUITab的父亲只能是WebUI
 * prefix：当前WebUITab的前缀，prefix将与上级节点的路径一起构成当前WebUITab的访问路径
 * A tab that represents a collection of pages.
 * The prefix is appended to the parent address to form a full path, and must not contain slashes.
 */
private[spark] abstract class WebUITab(parent: WebUI, val prefix: String) {
  //pages：当前WebUITab所包含的WebUIPage的缓冲数组
  val pages = ArrayBuffer[WebUIPage]()
  //name:当前WebUITab的名称，name实际是将prefix的首字母转换称大写字母后取得
  val name = prefix.capitalize

  /** Attach a page to this tab. This prepends the page's prefix with the tab's own prefix. */
  def attachPage(page: WebUIPage) {
    //将当前WebUITab的前缀与WebUIPage的前缀相拼接，作为WebUIPage的访问路径，
    page.prefix = (prefix + "/" + page.prefix).stripSuffix("/")
    //然后向Pages中添加WebUIPage
    pages += page
  }

  /** Get a list of header tabs from the parent UI. */
    //获取父亲WebUI中的所有WebUITab。此方法实际调用父亲WebUI的getTabs方法实现
  def headerTabs: Seq[WebUITab] = parent.getTabs
//获取父亲WebUI的基本路径
  def basePath: String = parent.getBasePath
}


/**
 * WebUiPage是WebUI框架体系的叶节点，定义了所有页面应当遵循的规范
 * WebUIPage在WebUi框架体系中的上一级节点可以是WebUI或者WebUITab，
 * 其成员属性prefix将与上级节点的路径一起构成当前WebUIPage的访问路径
 * A page that represents the leaf node in the UI hierarchy.
 *
 * The direct parent of a WebUIPage is not specified as it can be either a WebUI or a WebUITab.
 * If the parent is a WebUI, the prefix is appended to the parent's address to form a full path.
 * Else, if the parent is a WebUITab, the prefix is appended to the super prefix of the parent
 * to form a relative path. The prefix must not contain slashes.
 */
private[spark] abstract class WebUIPage(var prefix: String) {
  //WebUIPage定义了两个方法
  //1、渲染页面
  def render(request: HttpServletRequest): Seq[Node]
  //2、生成Json
  def renderJson(request: HttpServletRequest): JValue = JNothing
}
